短暫的中場休息 Q&A 練習後,開始進入第三個篇章 —— Resource Management。這次的 5 個知識點還算直觀,所以我們稍微加速一次講完,為後續更具挑戰的內容做好準備!
在經過前幾個章節的淬鍊後,大家都知曉清理資源的重要性。然而直接使用 delete
並不安全!如同下方的範例,若中途 return
或發生 exception,資源可能無法正常釋放而導致洩漏:
void f()
{
Investment *pInv = createInvestment();
...
delete pInv;
}
為了解決這個問題,我們應把刪除 object 的操作放到它的 destructor,這樣只要程式離開 f()
就會被自動清除。可以使用智慧指標 auto_ptr
,destructor 會自主對其指向的 object 呼叫 delete
:
void f()
{
std::auto_ptr<Investment> pInv(createInvestment());
...
}
說到智慧指標,就順勢介紹兩種設計原則:
auto_ptr
std::unique_ptr
:確保資源的「唯一所有權」,禁止複製只允許 std::move
轉移。std::shared_ptr
:用於多個物件共享同一資源,透過「引用計數」自動管理資源的生命週期。複製 RAII 物件時需要複製其管理的資源。針對 resource-managing class 的複製行為,有以下幾種可能的方式:
shared_ptr
允許多個 RAII 類共享一個資源。注意 shared_ptr
默認在引用計數歸零後會釋放資源,但對於像 mutex
鎖這種需要特別操作的資源,需要初始化時傳入自定義的刪除函式來正確釋放資源。std::move
等語意確保資源的管理權在不同物件間正確轉移。為了應對大量可能需要調用的 API,使用者必須能獲取 resource-managing class 管理的原始資源 —— 例如指標或物件。
get()
、重載 *
、->
運算子Resource-managing class 的目標是管理資源的獲取和釋放,所以無需考量直接訪問原始資源是否會破壞其封裝性。前面提到的 RAII 設計原則,就是為了確保釋放資源等操作會確實執行。
new
and delete
new
會配置一塊空間並調用 constructor,而 delete
則會調用 destructor 並釋放內存。當創建物件陣列時,編譯器會記錄 array 的長度,並保存在此記憶體的前部位置。釋放時需用 delete []
明確表示釋放的是 array,否則可能會被錯誤地視為單一 object。
因此 new
和 delete
應配套使用,避免未定義行為。如果在 new
表達式中使用 []
創建陣列,那麼在對應的 delete
表達式中也必須使用 []
。
即使使用智慧指標,若程式碼沒做好保護,在某些情況下仍可能有內存洩漏的風險:
processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());
在上述程式碼中,編譯器會先對函式參數進行求值,但並未規定具體順序。這可能導致 new Widget
先被執行,而在處理 priority()
時拋出 exception。此時智慧指標的構造函數尚未調用,導致內存洩漏。
想避免這個問題,可先在獨立語句中保存智慧指標,然後再將其傳入函式:
std::tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());